<?
/**
 * Copyright 2007 Melange.
 *
 * This file is part of Melange_Framework
 *
 * PHP-HTTP is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * PHP-HTTP is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PHP-HTTP; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @package framework
 *
 */


/**
 * Imports
 */
require_once("http/Request.php");
require_once("http/Response.php");
require_once("log/LoggerManager.php");


/**
 * Context class
 *
 * The framework context class
 *
 * @category    Melange Framework
 * @package     http
 * @copyright   Copyright (c) 2007 Jeroen Simons. All rights reserved
 * @author      Jeroen Simons <jeroen@melange.nl>
 * @link        http://www.melange.nl/
 *
 */
class Context {


    // -------------------------------------------------------------- Constants


    /**
     * The application path
     *
     */
    const APP_PATH = "_application";
    
    
    /**
     * Before cache action hook  
     */
    const ACTION_AFTER_CACHE = "after-cache";
    
    /**
     * Before cache action hook  
     */
    const ACTION_BEFORE_CACHE = "before-cache";


    /**
     * The cache path
     *
     */
    const CACHE_PATH = "_cache";


    /**
     * scheme constants
     */
    const CONTENT_TYPE  = "text/html; charset=UTF-8";


    /**
     * The request attribute under which we forward an HTTP status message
     * (as an object of type STring) to an error page.
     */
    const ERROR_MESSAGE_ATTR = "php.servlet.error.message";


    /**
     * The request attribute under which we forward a Java exception
     * (as an object of type Throwable) to an error page.
     */
    const EXCEPTION_ATTR = "javax.servlet.error.exception";


    /**
     * The request attribute under which we forward the request URI
     * (as an object of type String) of the page on which an error occurred.
     */
    const EXCEPTION_PAGE_ATTR =
        "nl.melange.http.error.request_uri";


    /**
     * The library path
     *
     */
    const LIBRARY_PATH = "_library";


    /**
     * Context log level
     */
    const LOG_LEVEL = "ERROR";


    /**
     * Context log file path
     */
    const LOG_FILE = "_log/framework.log";


    /**
     * The request attribute under which we forward an HTTP status code
     * (as an object of type Integer) to an error page.
     */
    const STATUS_CODE_ATTR =
        "php.servlet.error.status_code";


    // ----------------------------------------------------- Instance Variables


    /**
     * The context attributes for this context.
     */
    private $attributes = array();


    /**
     * The available authenticator mapped to a class.
     *
     * @var array
     */
    private $authenticators = array(
        "BASIC" => "BasicAuthenticator",
        "FORM"  => "FormAuthenticator"
    );


    /**
     * The basepath
     *
     * @var string
     */
    private $basePath;


    /**
     * The cache system
     *
     * @var Zend_Cache_Frontend_Output
     */
    private $cache;


    /**
     * The page cache exclude uris
     *
     * @var array
     */
    private $cacheExcludes = array();


    /**
     * The security constraints for this web application.
     */
    private $constraints = array();
    
    
    /**
     * Actionhooks
     *
     * @var array
     */
    private $actions = array();


    /**
     * The context logger
     *
     * @var LogCategory
     */
    private $log;


    /**
     * The context context path
     *
     * @var String
     */
    private $path;


    /**
     * The context baseUrl
     *
     * @var String
     */
    private $baseUrl;


    /**
     * The http request
     *
     * @var Request
     */
    private $request;


    /**
     * The Realm with which this Container is associated.
     */
    protected $realm = null;


    /**
     * The http response
     *
     * @var Response
     */
    private $response;


    /**
     * The status code error pages for this web application, keyed by
     * HTTP status code (as an Integer).
     */
    private $statusPages = array();


    /**
     * Use the zend cache system
     *
     * @var boolean
     */
    private $useCache = false;


    // ----------------------------------------------------------- Constructors


    /**
     * Construct a new context
     *
     * @return void
     */
    public function __construct($basePath, $contextPath) {
        $this->basePath = $basePath;
        $this->path = $contextPath;
    }


    // --------------------------------------------------------- Public Methods


    /**
     * Initialize the context
     *
     * @return void
     */
    public function init() {

        $library_path = $this->basePath."/".
                        (is_null($this->getInitParameter("LIBRARY_PATH")) ?
                        self::LIBRARY_PATH : $this->getInitParameter("LIBRARY_PATH"));

        $view_path    = !is_null($this->getInitParameter("VIEW_PATH")) ?
                            (PATH_SEPARATOR.$this->basePath."/".$this->getInitParameter("VIEW_PATH")) : "";

        // Set the include paths
        set_include_path(
            $this->basePath.        // The basepath
            PATH_SEPARATOR.         // Separator
            $library_path.          // Library path
            PATH_SEPARATOR.         // Separator
            dirname(__FILE__).      // The framework path
            $view_path              // A optional view path

        );

        $this->initMagicQuotes();
        $this->initLog();
        $this->initRequest();
        $this->initResponse();

    }


    /**
     * Process this request
     *
     * @return void
     */
    public function process() {

        // Is there a realm ?
        if(!is_null($this->realm)) {

            // Authenticate
            $this->authenticate();

            $this->log->debug("Suspended: " .$this->response->isSuspended());
            $this->log->debug("Error: " .$this->response->isError());
            $this->log->debug("Status: " .$this->response->getStatus());

            // Process the HTTP status
            $this->processError();
        }

        // No security applies and there are no errors.
        if(!$this->response->isSuspended() && !$this->response->isError()) {

            // cachable ?
            if(in_array($this->request->getPathInfo(), $this->cacheExcludes))
            	$this->setUseCache(false);
               
            $this->hook(self::ACTION_BEFORE_CACHE);

            // See if we have a cached resource for this request.
            if(!$this->useCache || is_null($this->getCache()) ||
               !$this->cache->start($this->getCacheKey())) {
               	
               	$this->hook(self::ACTION_AFTER_CACHE);

                // Include the PHP-MVC requirements.
                require_once("mvc/MVC.php");

                // Start the MVC Controller.
                $mvc = new MVC($this);

                // Process the Request
                $mvc->process($this->request, $this->response);

                // If successful finish up and cache.
                if($this->response->isError() || is_null($this->cache)) {

                    // Process the HTTP status
                    $this->processError();

                    // Finish
                    $this->response->finishResponse();
                    return;

                } else {

                    // Finish
                    $this->response->finishResponse();

                    // Add to cache
                    if($this->useCache) {
                        $this->cache->end(array("page"));
                        if($this->log->isDebugEnabled())
                            $this->log->debug("Caching new document on ".time());
                    }
                    return;

                }

            } else {

                if($this->log->isDebugEnabled())
                    $this->log->debug("Serving a cached document");
//                return;

            }

        }

        $this->response->finishResponse();

    }
    
    private function hook($action) {
    	if(array_key_exists($action, $this->actions)) {
	    	try {    		
	    		$function = $this->actions[$action];
	    		$function($this);
	        } catch(Exception $e) {    		
	        }
    	}
    }
    
    
    public function addAction($action, $function) {
    	$this->actions[$action] = $function; 
    }


    /**
     * Return the value of the specified initialization parameter, or
     * <code>null</code> if this parameter does not exist.
     *
     * @param name Name of the initialization parameter to retrieve
     */
    public function getInitParameter($name) {
        return array_key_exists($name, $this->parameters) ? $this->parameters[$name] : null;
    }


    /**
     * Set a specified initialization parameter
     *
     * @param name Name of the initialization parameter to retrieve
     */
    public function setInitParameter($name, $value) {
        $this->parameters[$name] = $value;
    }

    public function getInitParameterNames() {
        return array_keys($this->parameters);
    }


    // ------------------------------------------------------------- Properties


    /**
     * Return the value of the specified context attribute, if any;
     * otherwise return <code>null</code>.
     *
     * @param name Name of the context attribute to return
     */
    public function getAttribute($name) {
        return array_key_exists($name, $this->attributes) ? $this->attributes[$name] : null;
    }


    /**
     * Bind the specified value with the specified context attribute name,
     * replacing any existing value for that name.
     *
     * @param name Attribute name to be bound
     * @param value New attribute value to be bound
     */
    public function setAttribute($name, $value) {

        // Name cannot be null
        if (is_null($name))
            throw new Exception("Name cannot be null");

        // Null value is the same as removeAttribute()
        if (is_null($value)) {
            $this->removeAttribute($name);
            return;
        }

        $this->attributes[$name] = $value;
    }


    /**
     * Remove the context attribute with the specified name, if any.
     *
     * @param name Name of the context attribute to be removed
     */
    public function removeAttribute($name) {

        $found = array_key_exists($name, $this->attributes);
        if ($found) {
            unset($this->attributes[$name]);
        } else {
            return;
        }

    }


    /**
     * Return the value of the specified context attribute, if any;
     * otherwise return <code>null</code>.
     *
     * @param name Name of the context attribute to return
     */
    public function getAttributeNames() {
        return array_keys($this->attributes);
    }


    /**
     * @return Zend_Cache_Frontend_Output
     */
    public function getCache() {

        if(is_null($this->cache)) {
            $this->initCache();
        }

        return $this->cache;
    }


    /**
     * @param Zend_Cache_Frontend_Output $cache
     */
    public function setCache($cache) {
        $this->cache = $cache;
    }


    /**
     * @return array
     */
    public function getCacheExcludes() {
        return $this->cacheExcludes;
    }

    /**
     * @param array $cacheExcludes
     */
    public function setCacheExcludes($cacheExcludes) {
        $this->cacheExcludes = $cacheExcludes;
    }
	

    /**
	 * @return LogCategory
	 */
	public function getLog() {
		return $this->log;
	}
	
	/**
	 * @param LogCategory $log
	 */
	public function setLog($log) {
		$this->log = $log;
	}

    /**
     * Return the context path for this web application.
     */
    public function getPath() {
        return $this->path;
    }

    
	/**
	 * @return Request
	 */
	public function getRequest() {
		return $this->request;
	}


    /**
     * @return boolean
     */
    public function getUseCache() {
        return $this->useCache;
    }


    /**
     * @param boolean $useCache
     */
    public function setUseCache($useCache) {
        $this->useCache = $useCache;
    }


    // ------------------------------------------------------ Protected Methods


    /**
     * Initialize the page ( by requesturi ) cache.
     *
     */
    protected function initCache() {

        if(!is_null($this->cache))
            return;

        $frontendOptions = array(
           'lifeTime' => 60 * 60 * 24 * 356, // cache lifetime of 1 year
           'automatic_serialization' => false
        );

        $backendOptions = array(
            'cache_dir' => $this->basePath."/".self::CACHE_PATH."/" // Directory where to put the cache files
        );

        // Create lifetime cache.
        require_once("_library/Zend/Cache.php");
        $this->cache = Zend_Cache::factory('Output', 'File',
           $frontendOptions, $backendOptions);

    }


    protected function getCacheKey() {
        $url = (str_replace(array("/", ".", "&", "?", "=", "-", "%", ":"), "_", $_SERVER["REQUEST_URI"]));// .
            "_".strval($this->request->getLocale()->getLanguage());
        return "wb".$url;
    }


    /**
     * Initialize context http request.
     *
     * @return void
     */
    protected function initRequest() {
        $this->request = new Request($_SERVER["REQUEST_URI"]);
        $this->request->setContext($this);
    }


    /**
     * Initialize context http response.
     *
     * @return void
     */
    protected function initResponse() {

        $this->response = new Response();
        $this->response->setRequest($this->request);

        // Set the default response content-type
        $this->response->setContentType(
            is_null($this->getInitParameter("CONTENT_TYPE")) ? self::CONTENT_TYPE :
                $this->getInitParameter("CONTENT_TYPE"));
    }


    /**
     * Initialize context logger.
     *
     * @return void
     */
    protected function initLog() {

        if(is_null($this->log)) {

            // Set default log level if none given.
            $level = is_null($this->getInitParameter("LOG_LEVEL")) ? 
                self::LOG_LEVEL : $this->getInitParameter("LOG_LEVEL");
                
            // Set default log path if none given.
            $file = is_null($this->getInitParameter("LOG_FILE")) ?
                $this->basePath."/".self::LOG_FILE : $this->getInitParameter("LOG_FILE");

            LoggerManager::setFile($file);
            LoggerManager::setLevel($level);

            $this->log = LoggerManager::getLogger("Context");
        }
    }


    // ------------------------------------------------------- Security Methods


    /**
     * Return the Realm with which this Container is associated.  If there is
     * no associated Realm, return the Realm associated with our parent
     * Container (if any); otherwise return <code>null</code>.
     */
    public function getRealm() {

        if ($this->realm !== null)
            return ($this->realm);
        return (null);

    }


    /**
     * Set the Realm with which this Container is associated.
     *
     * @param realm The newly associated Realm
     */
    public function setRealm(RealmBase $realm) {

        $this->realm = $realm;

        // Start the new component if necessary
        if ($this->realm !== null)
            $this->realm->setApplicationContext($this);

    }


    /**
     * Return the login configuration descriptor for this web application.
     */
    public function getLoginConfig() {
        return $this->loginConfig;
    }


    /**
     * Set the login configuration descriptor for this web application.
     *
     * @param config The new login configuration
     */
    public function setLoginConfig(LoginConfig $config) {
        $this->loginConfig = $config;
    }


    /**
     * Add a security constraint to the set for this web application.
     */
    public function addConstraint(SecurityConstraint $constraint) {

        // Validate the proposed constraint
        $collections = $constraint->findCollections();
        for ($i = 0; $i < count($collections); $i++) {
            $patterns = $collections[$i]->findPatterns();
            for ($j = 0; $j < count($patterns); $j++) {
                $patterns[$j] = $this->adjustURLPattern($patterns[$j]);
                if (!$this->validateURLPattern($patterns[$j]))
                    throw new Exception
                        ("Invalid <url-pattern> ".$patterns[$j]." in security constraint");
            }
        }

        // Add this constraint to the set for our web application
        $this->constraints[] = $constraint;

    }


    /**
     * Return the security constraints for this web application.
     * If there are none, a zero-length array is returned.
     */
    public function findConstraints() {

        return ($this->constraints);

    }


    /**
     * Set up an Authenticator automatically if required, and one has not
     * already been configured.
     */
    public function authenticate() {

        // Does this Context require an Authenticator?
        // Todo: Support Security Contraints!
        $constraints = $this->findConstraints();
        if (($constraints === null) || (count($constraints) == 0))
            return;

        $loginConfig = $this->getLoginConfig();
        if ($loginConfig === null) {
            $loginConfig = new LoginConfig("NONE", null, null, null);
            $this->setLoginConfig(loginConfig);
        }

        // Has a Realm been configured for us to authenticate against?
        if ($this->getRealm() === null) {
            $this->log->error("No Realm has been configured to authenticate against");
            return;
        }

        // Identify the class name of the Valve we should configure
        $authenticatorName = array_key_exists($loginConfig->getAuthMethod(), $this->authenticators) ?
                                $this->authenticators[$loginConfig->getAuthMethod()] : null;

        if ($authenticatorName === null) {
            $this->log->error("Cannot configure an authenticator for method ".$loginConfig->getAuthMethod());
            return;
        }

        // Instantiate and install an Authenticator of the requested class
        try {

            require_once("aas/authenticator/".$authenticatorName.".php");
            $authenticator = new $authenticatorName();
            $authenticator->setContainer($this);
            $authenticator->invoke($this->request, $this->response, $this);

        } catch (Exception $e) {
            $this->log->error("Cannot instantiate an authenticator of class ".$authenticatorName. " ".$e->getMessage());
        }

    }


    // ------------------------------------------------- Error Handling Methods


    /**
     * Add an error page for the specified error or Java exception.
     *
     * @param errorPage The error page definition to be added
     */
    public function addErrorPage(ErrorPage $errorPage) {
        // Validate the input parameters
        if ($errorPage === null)
            throw new Exception("ErrorPage cannot be null");
        $location = $errorPage->getLocation();
        if (($location !== null) && (strpos($location, "/") !== 0)) {
            if($this->log->isDebugEnabled())
                $this->log->debug("WARNING: Error page location ".$location." must start with a ''/''");
            $errorPage->setLocation("/" . $location);
        }

        // Add the specified error page to our internal collections
        $exceptionType = $errorPage->getExceptionType();
        if ($exceptionType !== null) {
            $this->exceptionPages[$exceptionType] = $errorPage;
        } else {
            if ($errorPage->getErrorCode() == 200) {
                $this->okErrorPage = $errorPage;
            }
            $this->statusPages[intval($errorPage->getErrorCode())] = $errorPage;
        }
    }



    /**
     * Return the error page entry for the specified HTTP error code,
     * if any; otherwise return <code>null</code>.
     *
     * @param errorCode Error code to look up
     */
    public function findErrorPage($errorCode, $exceptionType = null) {

        if($exceptionType !== null)
            return array_key_exists($exceptionType, $this->exceptionPages)
                ? $this->exceptionPages[$exceptionType] : null;

        if ($errorCode == 200) {
            return ($this->okErrorPage);
        } else {
            return (array_key_exists($errorCode, $this->statusPages) ?
                $this->statusPages[intval($errorCode)] : null);
        }

    }


    /**
     * Return the set of defined error pages for all specified error codes
     * and exception types.
     */
    public function findErrorPages() {

        $results = array();
        $results += array_values($this->exceptionPages);
        $results += array_values($this->statusPages);
        return ($results);

    }


    /**
     * Process error
     *
     */
    public function processError() {

        /* Only look for error pages when isError() is set.
         * isError() is set when response.sendError() is invoked. This
         * allows custom error pages without relying on default from
         * web.xml.
         */
        if (!$this->response->isError())
            return;

        $this->response->setSuspended(false);
        $t = $this->request->getAttribute(self::EXCEPTION_ATTR);
        if ($t !== null) {
            $this->throwable($t);
        } else {
            $this->status();
        }
    }


    /**
     * Handle the HTTP status code (and corresponding message) generated
     * while processing the specified Request to produce the specified
     * Response.  Any exceptions that occur during generation of the error
     * report are logged and swallowed.
     *
     * Note: taken from StandardValveHost.java
     *
     * @param request The request being processed
     * @param response The response being generated
     */
    public function status() {

        $statusCode = $this->response->getStatus();

        /* Only look for error pages when isError() is set.
         * isError() is set when response.sendError() is invoked. This
         * allows custom error pages without relying on default from
         * web.xml.
         */
        if (!$this->response->isError())
            return;

        $errorPage = $this->findErrorPage($statusCode);

        if ($errorPage !== null) {
            $this->request->setAttribute(self::STATUS_CODE_ATTR,
                              intval($statusCode));

            $message = htmlentities($this->response->getMessage());
            if ($message === null)
                $message = "";
            $this->request->setAttribute(self::ERROR_MESSAGE_ATTR, $message);
//            $this->request->setAttribute(self::DISPATCHER_REQUEST_PATH_ATTR, $errorPage->getLocation());
//            $this->request->setAttribute(self::DISPATCHER_TYPE_ATTR, intval(self::ERROR));
            $this->request->setAttribute(self::EXCEPTION_PAGE_ATTR, $this->request->getRequestURI());
            if ($this->custom($errorPage)) {
                try {
//                    $this->response->flushBuffer();
                } catch (Exception $e) {
                    $this->log->warn("Exception Processing " . $errorPage ." : " . $e);
                }
            }
            $this->response->setSuspended(true);
        }

    }


    /**
     * Handle the specified Throwable encountered while processing
     * the specified Request to produce the specified Response.  Any
     * exceptions that occur during generation of the exception report are
     * logged and swallowed.
     *
     * @param request   The request being processed
     * @param response  The response being generated
     * @param throwable The exception that occurred (which possibly wraps
     *                  a root cause exception
     */
    protected function throwable($throwable) {

        $context = $this->request->getContext();
        if ($context === null)
            return;

        $errorPage = $this->findErrorPage($context, get_class($throwable));

        if ($errorPage !== null) {

            $this->response->setAppCommitted(false);
            $this->request->setAttribute(self::STATUS_CODE_ATTR, Response::SC_INTERNAL_SERVER_ERROR);
            $this->request->setAttribute(self::ERROR_MESSAGE_ATTR, $throwable->getMessage());
            $this->request->setAttribute(self::EXCEPTION_ATTR, $throwable);
//            $this->request->setAttribute(self::DISPATCHER_REQUEST_PATH_ATTR, $errorPage->getLocation());
//            $this->request->setAttribute(self::DISPATCHER_TYPE_ATTR, intval(self::ERROR));
            $this->request->setAttribute(self::EXCEPTION_PAGE_ATTR, $this->request->getRequestURI());
            if ($this->custom($errorPage)) {
                try {
                    $this->response->flushBuffer();
//                    $this->response->setSuspended(true);
                    exit();
                } catch (Exception $e) {
                    $this->log->warn("Exception Processing " . $errorPage ." : " . $e);
                }
            }

        } else {
            // A custom error-page has not been defined for the exception
            // that was thrown during request processing. Check if an
            // error-page for error code 500 was specified and if so,
            // send that page back as the response.
            $this->response->setStatus(Response::SC_INTERNAL_SERVER_ERROR);
            // The response is an error
            $this->response->setError();

            $this->status();
        }


    }


    /**
     * Handle an HTTP status code or Java exception by forwarding control
     * to the location included in the specified errorPage object.  It is
     * assumed that the caller has already recorded any request attributes
     * that are to be forwarded to this page.  Return <code>true</code> if
     * we successfully utilized the specified error page location, or
     * <code>false</code> if the default error report should be rendered.
     *
     * @param request The request being processed
     * @param response The response being generated
     * @param errorPage The errorPage directive we are obeying
     */
    protected function custom(ErrorPage $errorPage) {

        if ($this->log->isDebugEnabled())
            $this->log->debug("Processing " . $errorPage);

        //$this->request->setPathInfo($errorPage->getLocation());

        try {

            // Reset the response if possible (else IllegalStateException)
            //hres.reset();
            // Reset the response (keeping the real error code and message)
            $statusCodeObj =
                $this->request->getAttribute(self::STATUS_CODE_ATTR);
            $statusCode = intval($statusCodeObj);
            $message =
                $this->request->getAttribute(self::ERROR_MESSAGE_ATTR);

            $this->response->reset($statusCode, $message);

            if(file_exists($this->basePath.$errorPage->getLocation()))
                include($this->basePath.$errorPage->getLocation());
            else
                throw new Exception("Could not find error page (".
                    $this->basePath.$errorPage->getLocation().")");

            // If we forward, the response is suspended again
            $this->response->setSuspended(false);

            // Indicate that we have successfully processed this custom page
            return true;

        } catch (Exception $t) {
            // Report our failure to process this custom page
            $this->log->error("Exception Processing " . $errorPage . " " . $t);
        }

        return false;

    }


    // ----------------------------------------------- File & Directory Methods


    /**
     * Return the real path for a given virtual path, if possible; otherwise
     * return <code>null</code>.
     *
     * @param path The path to the desired resource
     */
    public function getRealPath($path) {
        return ($this->basePath.$path);
    }


    /**
     * Loads a PHP file.  This is a wrapper for PHP's include() function.
     *
     * $filename must be the complete filename, including any
     * extension such as ".php".  Note that a security check is performed that
     * does not permit extended characters in the filename.  This method is
     * intended for loading Zend Framework files.
     *
     * If $dirs is a string or an array, it will search the directories
     * in the order supplied, and attempt to load the first matching file.
     *
     * If the file was not found in the $dirs, or if no $dirs were specified,
     * it will attempt to load it from PHP's include_path.
     *
     * If $once is TRUE, it will use include_once() instead of include().
     *
     * @param  string        $filename
     * @param  string|array  $dirs
     * @param  boolean       $once
     * @throws Zend_Exception
     * @return mixed
     */
    static public function loadFile($filename, $dirs=null, $once=false) {

        // security check
        if (preg_match('/[^a-z0-9\-_.]/i', $filename)) {
            throw new Exception('Security check: Illegal character in filename');
        }

        /**
         * Determine if the file is readable, either within just the include_path
         * or within the $dirs search list.
         */
        $filespec = $filename;
        if ($dirs === null) {
            $found = self::isReadable($filespec);
        } else {
            foreach ((array)$dirs as $dir) {
                $filespec = rtrim($dir, '\\/') . DIRECTORY_SEPARATOR . $filename;
                $found = self::isReadable($filespec);
                if ($found) {
                    break;
                }
            }
        }

        /**
         * Throw an exception if the file could not be located
         */
        if (!$found) {
            throw new Exception("File \"$filespec\" was not found.");
        }

        /**
         * Attempt to include() the file.
         *
         * include() is not prefixed with the @ operator because if
         * the file is loaded and contains a parse error, execution
         * will halt silently and this is difficult to debug.
         *
         * Always set display_errors = Off on production servers!
         */
        if ($once) {
            return include_once $filespec;
        } else {
            return $filespec ;
        }
    }


    /**
     * Loads a class from a PHP file.  The filename must be formatted
     * as "$class.php".
     *
     * If $dirs is a string or an array, it will search the directories
     * in the order supplied, and attempt to load the first matching file.
     *
     * If $dirs is null, it will split the class name at underscores to
     * generate a path hierarchy (e.g., "Zend_Example_Class" will map
     * to "Zend/Example/Class.php").
     *
     * If the file was not found in the $dirs, or if no $dirs were specified,
     * it will attempt to load it from PHP's include_path.
     *
     * @param string $class
     * @param string|array $dirs
     * @throws Zend_Exception
     * @return void
     */
    static public function loadClass($class, $dirs = null) {

        if (class_exists($class, false)) {
            return;
        }

        // autodiscover the path from the class name
        $path = str_replace('_', DIRECTORY_SEPARATOR, $class);
        if ($dirs === null && $path != $class) {
            // use the autodiscovered path
            $dirs = dirname($path);
            $file = basename($path) . '.php';
        } else {
            $file = $class . '.php';
        }

        self::loadFile($file, $dirs, true);

        if (!class_exists($class, false)) {
            throw new Zend_Exception("File \"$file\" was loaded "
                               . "but class \"$class\" was not found within.");
        }
    }


    static public function getFilePath($filename) {

        if (is_readable($filename)) {
            return $filename;
        }

        $path = get_include_path();
        $dirs = explode(PATH_SEPARATOR, $path);

        foreach ($dirs as $dir) {
            // No need to check against current dir -- already checked
            if ('.' == $dir) {
                continue;
            }

            if (is_readable($dir . DIRECTORY_SEPARATOR . $filename)) {
                return $dir . DIRECTORY_SEPARATOR . $filename;
            }
        }

        return $filename;
    }


    /**
     * Returns TRUE if the $filename is readable, or FALSE otherwise.  This
     * function uses the PHP include_path, where PHP's is_readable() does not.
     *
     * @param string $filename
     * @return boolean
     */
    static public function isReadable($filename) {

        if (is_readable($filename)) {
            return true;
        }

        $path = get_include_path();
        $dirs = explode(PATH_SEPARATOR, $path);

        foreach ($dirs as $dir) {
            // No need to check against current dir -- already checked
            if ('.' == $dir) {
                continue;
            }

            if (is_readable($dir . DIRECTORY_SEPARATOR . $filename)) {
                return true;
            }
        }

        return false;
    }


    // -------------------------------------------------------- Private Methods


    /**
     * Disable magic quotes.
     *
     */
    private static function initMagicQuotes() {
        if(get_magic_quotes_gpc()) {
            $_POST = self::stripSlashesDeep($_POST);
            $_GET = self::stripSlashesDeep($_GET);
            $_COOKIE = self::stripSlashesDeep($_COOKIE);
        }
        set_magic_quotes_runtime(FALSE);
    }


    /**
     * Recursive slash remover used by
     * Context::initMagicQuotes()
     *
     * @param array $value
     * @return array
     */
    private static function stripSlashesDeep($value) {
        if(is_array($value)) {
            $value = array_map('Context::stripSlashesDeep', $value);
        } else if (!empty($value) && is_string($value)) {
            $value = stripslashes($value);
        }
        return $value;
    }


   /**
     * Adjust the URL pattern to begin with a leading slash, if appropriate
     * (i.e. we are running a servlet 2.2 application).  Otherwise, return
     * the specified URL pattern unchanged.
     *
     * @param urlPattern The URL pattern to be adjusted (if needed)
     *  and returned
     */
    private function adjustURLPattern($urlPattern) {

        if ($urlPattern === null)
            return ($urlPattern);
        if (preg_match("/^\//", $urlPattern) || preg_match("/^\*\./", $urlPattern))
            return ($urlPattern);

        $this->log->warn("WARNING: URL pattern ".$urlPattern." must start with a '/' in Servlet 2.3");
        return ("/" . $urlPattern);

    }


    /**
     * Validate the syntax of a proposed <code>&lt;url-pattern&gt;</code>
     * for conformance with specification requirements.
     *
     * @param urlPattern URL pattern to be validated
     */
    private function validateURLPattern($urlPattern) {

        if ($urlPattern === null)
            return (false);
        if (preg_match("/^\*\./", $urlPattern)) {
            if (strpos($urlPattern, "/") === false)
                return (true);
            else
                return (false);
        }
        if (preg_match("/^\//", $urlPattern))
            return (true);
        else
            return (false);

    }



}
?>